﻿using System;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using Fiddler;
using NerdWare.Crm2011VsTools.Core;

namespace NerdWare.Crm2011VsTools.CrmScriptInjector
{
    /// <summary>
    /// Proxy Class to handle the Fiddler Core
    /// </summary>
    public sealed class ScriptProxy : IDisposable
    {
        private string _hostname;
        private int _port;
        private string _organization;
        private CrmType _crmType;

        private bool _fiddlerCertInstalled = false;
        private bool _fiddlerCertTrusted = false;

        private readonly FileMonitor _fileMonitor = new FileMonitor();

        /// <summary>
        /// Gets a value indicating whether this instance is running.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is running; otherwise, <c>false</c>.
        /// </value>
        public bool IsRunning
        {
            get { return FiddlerApplication.IsStarted(); }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ScriptProxy"/> class.
        /// </summary>
        public ScriptProxy()
        {
            NTrace.Default.Info("ScriptProxy initialized.");
            FiddlerApplication.BeforeRequest += ProxyBeforeRequest;
        }

        /// <summary>
        /// Starts the proxy.
        /// </summary>
        /// <param name="crmType">Type of the CRM.</param>
        /// <param name="hostname">The hostname.</param>
        /// <param name="port">The port.</param>
        /// <param name="organization">The organization.</param>
        /// <param name="fiddlerPort">The fiddler port.</param>
        /// <param name="folder">The folder.</param>
        public void StartProxy(CrmType crmType, string hostname, int port, string organization, int fiddlerPort, string folder)
        {
            if (!FiddlerApplication.IsStarted())
            {
                _crmType = crmType;
                _hostname = hostname.ToLower();
                _port = port;
                _organization = organization.ToLower();

                UriRegEx = generateRegEx();

                _fileMonitor.GetFiles(folder);

                FiddlerCoreStartupFlags captureFlags = FiddlerCoreStartupFlags.RegisterAsSystemProxy |
                                                       FiddlerCoreStartupFlags.MonitorAllConnections;
                if (port == 443)
                {
                    InstallCerts();
                    captureFlags = captureFlags | FiddlerCoreStartupFlags.DecryptSSL;
                }

                FiddlerApplication.Startup(fiddlerPort, captureFlags);

                NTrace.Default.Debug("Fiddler Proxy started.");
            }

        }

        /// <summary>
        /// Stops the proxy.
        /// </summary>
        public void StopProxy()
        {
            if (FiddlerApplication.IsStarted())
            {
                FiddlerApplication.Shutdown();
                NTrace.Default.Info("Fiddler Proxy stopped.");
            }
        }

        private void ProxyBeforeRequest(Session osession)
        {
            if (osession.hostname.ToLower() == _hostname && osession.port == _port)
            {
                if (osession.PathAndQuery.ToLower().Contains("webresources"))
                {
                    Match match = UriRegEx.Match(osession.PathAndQuery.ToLower());

                    if (match.Success)
                    {
                        NTrace.Default.Debug("PathAndQuery '{0}' matches.", osession.PathAndQuery);

                        string key = match.Groups[Groups.Filename].Value;

                            if (!key.EndsWith(".js"))
                                key = string.Concat(key, ".js");

                            switch (_fileMonitor.ContainCounter(key))
                            {
                                case 0:
                                    NTrace.Default.Debug("Match {0} has no possible matching Source-File.", key);
                                    break;
                                case 1:
                                    NTrace.Default.Info("Match {0} gets replaced by the local copy.", key);
                                    osession.bBufferResponse = true;
                                    osession.utilCreateResponseAndBypassServer();
                                    osession.oResponse.headers.HTTPResponseStatus = "200 Ok";
                                    osession.oResponse["Content-Type"] = "text/javascript; charset=UTF-8";
                                    osession.oResponse["Cache-Control"] = "private, max-age=0";
                                    osession.utilSetResponseBody(_fileMonitor.GetFileContent(key));
                                    break;
                                default:
                                    NTrace.Default.Warn("Match {0} has {1} possible matching Source-Files.", key,
                                                        _fileMonitor.ContainCounter(key));
                                    break;
                            }
                    }
                    else
                    {
                        NTrace.Default.Warn("PathAndQuery '{0}' was no Match in the Expression.",
                                            osession.PathAndQuery);
                    }
                }
                else
                {
                    NTrace.Default.Debug("PathAndQuery '{0}' doesn't contain the word 'webresources'", osession.PathAndQuery);
                }
            }
            else
            {
                NTrace.Default.Warn("Watching for Hostname '{0}' and Port [{1}] but found Hostname '{2}' and Port [{3}]",
                    _hostname, _port, osession.hostname, osession.port);
            }
        }

        private Regex UriRegEx { get; set; }

        private Regex generateRegEx()
        {
            switch (_crmType)
            {
                case CrmType.Crm2011OnPremise:
                case CrmType.Crm2011OnlineNorthAmericaLiveId:
                case CrmType.Crm2011OnlineNorthAmericaOffice365:
                case CrmType.Crm2011OnlineEMEALiveId:
                case CrmType.Crm2011OnlineEMEAOffice365:
                case CrmType.Crm2011OnlineAPACLiveId:
                case CrmType.Crm2011OnlineAPACOffice365:

                    return new Regex(@"(\/(?<" + Groups.Organization + @">" + _organization + @")){0,1}\/%7b\d+%7d\/webresources\/(?<" + Groups.Path + @">.+\/)*(?<" + Groups.Filename + @">.+(\.js){0,1})");
                default:
                    throw new NotImplementedException("This Crm Type is not yet implemented.");
            }
        }

        private void InstallCerts()
        {
            try
            {
                HighJackMakeCertPath(CertMaker.oCertProvider);

                if (!CertMaker.rootCertExists())
                {
                    NTrace.Default.Info("Certificates do not exist. Trying to install...");

                    if (!CertMaker.createRootCert())
                    {
                        throw new ApplicationException("Unable to create certificate for FiddlerCore.");
                    }
                    else
                    {
                        NTrace.Default.Info("Certificates installed successfully.");
                        _fiddlerCertInstalled = true;
                    }
                }

                if (!CertMaker.rootCertIsTrusted())
                {
                    NTrace.Default.Info("Certificate not trusted. Trying to trust...");

                    if (!CertMaker.trustRootCert())
                    {
                        throw new ApplicationException("Unable to install FiddlerCore's certificate.");
                    }
                    else
                    {
                        NTrace.Default.Info("Certificate is now trusted.");
                        _fiddlerCertTrusted = true;
                    }

                    _fiddlerCertTrusted = true;
                }
            }
            catch (Exception ex)
            {
                NTrace.Default.ErrorException(ex.Message, ex);
            }
        }

        private void RemoveCerts()
        {
            if (_fiddlerCertTrusted || _fiddlerCertInstalled)
            {
                CertMaker.removeFiddlerGeneratedCerts(true);

                _fiddlerCertInstalled = false;
                _fiddlerCertTrusted = false;
            }
        }

        private void HighJackMakeCertPath(ICertificateProvider certificateProvider)
        {
            Type myType = certificateProvider.GetType();

            const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;

            FieldInfo certLocation = myType.GetField("_sMakeCertLocation", flags);
            if (certLocation != null)
            {
                string makeCertLocation = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location).FullName, "MakeCert.Exe");

                if (certLocation.GetValue(certificateProvider).ToString() != makeCertLocation)
                {
                    NTrace.Default.Info("Overwriting MakeCert-Path from '{0}' with '{1}'", certLocation.GetValue(certificateProvider), makeCertLocation);
                    certLocation.SetValue(certificateProvider, makeCertLocation);
                }
            }
        }

        public void Dispose()
        {
            if (FiddlerApplication.IsStarted())
                FiddlerApplication.Shutdown();

            if (_port == 443)
            {
                RemoveCerts();
            }

            FiddlerApplication.BeforeRequest -= ProxyBeforeRequest;
        }
    }
}
